﻿#region File and License Information

/*
<File>
	<License Type="BSD">
		Copyright © 2009 - 2012, Outcoder. All rights reserved.
	
		This file is part of Calcium (http://calciumsdk.net).

		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions are met:
			* Redistributions of source code must retain the above copyright
			  notice, this list of conditions and the following disclaimer.
			* Redistributions in binary form must reproduce the above copyright
			  notice, this list of conditions and the following disclaimer in the
			  documentation and/or other materials provided with the distribution.
			* Neither the name of the <organization> nor the
			  names of its contributors may be used to endorse or promote products
			  derived from this software without specific prior written permission.

		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
		ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
		WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
		DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
		DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
		(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
		LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
		ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	</License>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com" />
	<CreationDate>2012-02-18 13:11:43Z</CreationDate>
</File>
*/

#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;

using Outcoder.Collections;
using Outcoder.Concurrency;
using Outcoder.Reflection;

namespace Outcoder.InversionOfControl
{
	/// <summary>
	/// This <c>IContainer</c> supports, singleton and non-singleton
	/// mapping from concrete types and interface types to concrete types,
	/// object instances, lambda expressions. 
	/// It also supports dependency injection.
	/// Unit tests can be found in the WPF unit test library.
	/// </summary>
	public class SimpleContainer : IContainer
	{
		readonly Dictionary<Type, ResolverDictionary> typeDictionary = new Dictionary<Type, ResolverDictionary>();
		readonly Dictionary<string, Type> keyDictionary = new Dictionary<string, Type>();

		static readonly string defaultKey = Guid.NewGuid().ToString();
		readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

		readonly Dictionary<Type, List<string>> cycleDictionary = new Dictionary<Type, List<string>>();

		readonly Dictionary<Type, ConstructorInvokeInfo> constructorDictionary
			= new Dictionary<Type, ConstructorInvokeInfo>();
		readonly Dictionary<Type, List<PropertyInfo>> injectablePropertyDictionary
								= new Dictionary<Type, List<PropertyInfo>>();
		readonly Dictionary<string, Action<object, object>> propertyActionDictionary
			= new Dictionary<string, Action<object, object>>();
		readonly Dictionary<Type, PropertyInfo[]> propertyDictionary
			= new Dictionary<Type, PropertyInfo[]>();

		readonly ReaderWriterLockSlim constructorDictionaryLockSlim
			= new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

		/// <summary>
		/// Prevents multiple threads from creating more 
		/// than one singleton instance.
		/// Decreases performance if <c>true</c>.
		/// Default value is <c>false</c>.
		/// </summary>
		public bool ThreadSafe { get; set; }

		/// <summary>
		/// If <c>true</c> the container resolves values for properties 
		/// that are decorated with a <seealso cref="InjectDependenciesAttribute"/>.
		/// There's a performance penalty for that. If you do not use
		/// <c>InjectDependenciesAttribute</c> for properties, 
		/// set this property to false to improve performance.
		/// Default value is <c>true</c>.
		/// </summary>
		public bool PropertyInjectionAttributesEnabled { get; set; } = true;

		sealed class ResolverDictionary : Dictionary<string, Resolver>
		{
		}

		sealed class Resolver
		{
			public Func<object> CreateInstanceFunc { get; set; }
			public object Instance;
			public bool Singleton;

			public object GetObject()
			{
				if (Singleton)
				{
					if (Instance != null)
					{
						return Instance;
					}

					Instance = CreateInstanceFunc();

					if (Instance != null)
					{
						CreateInstanceFunc = null;
					}

					return Instance;
				}

				return CreateInstanceFunc();
			}
		}

		public void Register<TFrom, TTo>(bool singleton = false, string key = null) where TTo : TFrom
		{
			Type fromType = typeof(TFrom);
			Type toType = typeof(TTo);
			Register(fromType, toType, singleton, key);
		}

		public void Register(Type fromType, Type toType, 
			bool singleton = false, string key = null)
		{
			key = GetKeyValueOrDefault(key);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary resolverDictionary;
				if (!typeDictionary.TryGetValue(fromType, out resolverDictionary))
				{
					resolverDictionary = new ResolverDictionary();
					typeDictionary[fromType] = resolverDictionary;
				}

				Resolver resolver = new Resolver
				{
					CreateInstanceFunc = () => Instantiate(toType),
					Singleton = singleton
				};
				resolverDictionary[key] = resolver;
				keyDictionary[key] = fromType;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public void Register<TInterface>(TInterface instance, string key = null)
		{
			key = GetKeyValueOrDefault(key);
			Type type = typeof(TInterface);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary value;
				if (!typeDictionary.TryGetValue(type, out value))
				{
					value = new ResolverDictionary();
				}

				value[key] = new Resolver { CreateInstanceFunc = () => instance };
				typeDictionary[type] = value;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public void Register<T>(Func<T> getInstanceFunc, bool singleton = false, string key = null)
		{
			ArgumentValidator.AssertNotNull(getInstanceFunc, "getInstanceFunc");

			key = GetKeyValueOrDefault(key);
			Type type = typeof(T);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary resolverDictionary;
				if (!typeDictionary.TryGetValue(type, out resolverDictionary))
				{
					resolverDictionary = new ResolverDictionary();
				}

				Resolver resolver = new Resolver { Singleton = singleton };

				Func<object> getObjectFunc = () =>
				{
					List<string> keys;
					if (cycleDictionary.TryGetValue(type, out keys))
					{
						if (keys.Contains(key))
						{
							throw new ResolutionException($"Cycle detected for {type} with key \"{key}\"");
						}

						keys.Add(key);
					}
					else
					{
						keys = new List<string> { key };
						cycleDictionary.Add(type, keys);
					}

					T result;
					try
					{
						result = getInstanceFunc();
					}
					finally
					{
						keys.Remove(key);
					}

					if (resolver.Singleton)
					{
						resolver.CreateInstanceFunc = null;
						resolver.Instance = result;
					}

					return result;
				};

				resolver.CreateInstanceFunc = getObjectFunc;
				
				resolverDictionary[key] = resolver;
				keyDictionary[key] = type;

				typeDictionary[type] = resolverDictionary;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public void Register(Type type, Func<object> getInstanceFunc, bool singleton = false, string key = null)
		{
			ArgumentValidator.AssertNotNull(type, "fromType");
			ArgumentValidator.AssertNotNull(getInstanceFunc, "getInstanceFunc");

			key = GetKeyValueOrDefault(key);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary resolverDictionary;
				if (!typeDictionary.TryGetValue(type, out resolverDictionary))
				{
					resolverDictionary = new ResolverDictionary();
				}

				Resolver resolver = new Resolver { Singleton = singleton };

				Func<object> getObjectFunc = () =>
				{
					var result = getInstanceFunc();

					if (resolver.Singleton)
					{
						resolver.CreateInstanceFunc = null;
						resolver.Instance = result;
					}

					return result;
				};

				resolver.CreateInstanceFunc = getObjectFunc;
				
				resolverDictionary[key] = resolver;
				keyDictionary[key] = type;

				typeDictionary[type] = resolverDictionary;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public void Register(Type type, object instance, string key = null)
		{
			ArgumentValidator.AssertNotNull(type, "type");
			key = GetKeyValueOrDefault(key);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary value;
				if (!typeDictionary.TryGetValue(type, out value))
				{
					value = new ResolverDictionary();
				}

				Resolver info = new Resolver { CreateInstanceFunc = () => instance };

				value[key] = info;

				typeDictionary[type] = value;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public bool IsRegistered<T>(string key = null)
		{
			return IsRegistered(typeof(T), key);
		}

		public bool IsRegistered(Type fromType, string key = null)
		{
			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				if (!string.IsNullOrEmpty(key))
				{
					/* Improve performance of this by using a lookup dictionary. */
					foreach (KeyValuePair<Type, ResolverDictionary> pair in typeDictionary)
					{
						if (!fromType.IsAssignableFrom(pair.Key))
						{
							continue;
						}

						var funcDictionary = pair.Value;
						if (pair.Value == null)
						{
							continue;
						}

						if (funcDictionary.ContainsKey(key))
						{
							return true;
						}
					}
				}

				ResolverDictionary dictionary;
				if (typeDictionary.TryGetValue(fromType, out dictionary) && dictionary != null)
				{
					if (dictionary.ContainsKey(key ?? defaultKey))
					{
						return true;
					}
				}
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			return false;
		}

		public T Resolve<T>(string key = null)
		{
			return (T)ResolveAux(typeof(T), key);
		}

		public object Resolve(Type type, string key = null)
		{
			ArgumentValidator.AssertNotNull(type, "type");
			return ResolveAux(type, key);
		}

		object ResolveAux(Type type, string key = null, Dictionary<string, object> resolvedObjects = null)
		{
			bool alreadyResolved = false;
			object result = null;
			if (resolvedObjects != null && key != null)
			{
				if (resolvedObjects.TryGetValue(key, out result))
				{
					alreadyResolved = true;
				}
			}

			if (!alreadyResolved)
			{
				result = ResolveCore(type, key);

				if (key != null && resolvedObjects != null)
				{
					resolvedObjects[key] = result;
				}

				if (result != null && PropertyInjectionAttributesEnabled)
				{
					ResolveProperties(result, resolvedObjects);
				}
			}

			if (resolvedObjects != null)
			{
				resolvedObjects.Clear();
			}

			return result;
		}

		public IEnumerable<object> ResolveAll(string key)
		{
			ArgumentValidator.AssertNotNull(key, "key");

			List<object> result = new List<object>();
			
			foreach (KeyValuePair<Type, ResolverDictionary> pair in typeDictionary)
			{
				var dictionary = pair.Value;
				Resolver resolver;
				if (dictionary.TryGetValue(key, out resolver))
				{
					var item = resolver.Instance ?? resolver.CreateInstanceFunc?.Invoke();
					result.Add(item);
				}
			}

			return result;
		}

		public IEnumerable<Object> ResolveAll(Type fromType)
		{
			ArgumentValidator.AssertNotNull(fromType, "fromType");
			var list = new List<object>();

			ResolverDictionary resolverDictionary;
			bool retrievedValue;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				retrievedValue = typeDictionary.TryGetValue(fromType, out resolverDictionary);
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			if (!retrievedValue)
			{
				return list;
			}

			if (resolverDictionary == null)
			{
				object builtObject = BuildUp(fromType, null);
				if (builtObject != null)
				{
					list.Add(builtObject);
				}
			}
			else
			{
				foreach (Resolver resolver in resolverDictionary.Values)
				{
					var item = resolver.GetObject();
					list.Add(item);
				}
			}

			return list;
		}

		public IEnumerable<TFrom> ResolveAll<TFrom>() where TFrom : class
		{
			Type fromType = typeof(TFrom);
			List<TFrom> list = new List<TFrom>();

			ResolverDictionary dictionary;
			bool retrieved;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				retrieved = typeDictionary.TryGetValue(fromType, out dictionary);
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}
			
			if (!retrieved)
			{
				return list;
			}

			//			if (dictionary == null)
			//			{
			//#if NETFX_CORE
			//				if (!fromType.GetTypeInfo().IsInterface)
			//#else
			//				if (!fromType.IsInterface)
			//#endif
			//				{
			//					foreach (var pair in singletonTypes)
			//					{
			//						var typeKey = pair.Key;
			//						if (typeKey.ObjectType == fromType)
			//						{
			//							Type toType = pair.Value;
			//							object o = BuildUp(toType, typeKey.Key);
			//							if (o != null)
			//							{
			//								list.Add((TFrom)o);
			//							}
			//						}
			//					}
			//				}
			//			}
			//			else
			//			{
			//				list.AddRange(dictionary.Values.Select(resolveFunc => resolveFunc.GetObject()));
			//			}

			//list.AddRange(dictionary.Values.Select(resolveFunc => resolveFunc.GetObject()));

			foreach (var resolver in dictionary.Values)
			{
				var item = (TFrom)resolver.GetObject();
				list.Add(item);
			}

			return list;
		}

		object ResolveCore(Type type, string key)
		{
			key = GetKeyValueOrDefault(key);

			if (type == null)
			{
				type = ResolveType(key);

				if (type == null)
				{
					throw new ResolutionException("Failed to resolve type for " + key);
				}
			}
			
			ResolverDictionary resolvers;
			Resolver resolver = null;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				if (typeDictionary.TryGetValue(type, out resolvers) && resolvers != null)
				{
					/* If the ResolveProperties method calls through here, it may be searching for a registration per
					 * strongly-typed name that does not exist. In this case, just get the default registration. */
					if (!resolvers.TryGetValue(key, out resolver))
					{
						key = GetKeyValueOrDefault(null);
						resolver = resolvers[key];
					}
				}
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			if (resolvers == null)
			{
				return BuildUp(type, key);
			}

			if (resolver != null)
			{
				return resolver.GetObject();
			}

			return BuildUp(type, key);
		}

		Type ResolveType(string key)
		{
			Type result = GetTypeFromContainer(key);
			if (result != null)
			{
				return result;
			}

			/*  Not in the container? Try the Assembly. */
#if NETFX_CORE
			return GetType().GetTypeInfo().Assembly.GetType(key);
#else
			return Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(t => t.Name == key);
#endif
		}

		Type GetTypeFromContainer(string key)
		{
			Type result;
			if (!keyDictionary.TryGetValue(key, out result))
			{
				return null;
			}

			return result;
		}

		void ResolveProperties(object instance, Dictionary<string, object> resolvedObjects)
		{
			Type type = instance.GetType();
			List<PropertyInfo> injectableProperties;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				bool hasCachedInjectableProperties = injectablePropertyDictionary.TryGetValue(type, out injectableProperties);

				if (!hasCachedInjectableProperties)
				{
					PropertyInfo[] properties;
					bool hasCachedProperties = propertyDictionary.TryGetValue(type, out properties);

					if (!hasCachedProperties)
					{
#if NETFX_CORE
						properties = type.GetTypeInfo().DeclaredProperties.ToArray();
#else
						properties = type.GetProperties();
#endif
						propertyDictionary[type] = properties;
					}

					injectableProperties = new List<PropertyInfo>();

					foreach (PropertyInfo propertyInfo in properties)
					{
						var dependencyAttributes = propertyInfo.GetCustomAttributes(typeof(InjectDependenciesAttribute), false);
#if NETFX_CORE
						if (!dependencyAttributes.Any())
#else
						if (dependencyAttributes.Length < 1) /* Faster than using LINQ. */
#endif
						{
							continue;
						}

						injectableProperties.Add(propertyInfo);
					}

					injectablePropertyDictionary[type] = injectableProperties;
				}
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			if (injectableProperties == null || injectableProperties.Count < 1)
			{
				return;
			}

			if (resolvedObjects == null)
			{
				resolvedObjects = new Dictionary<string, object>();
			}

			string typeName = type.FullName + ".";

			foreach (PropertyInfo propertyInfo in injectableProperties)
			{
				string fullPropertyName = typeName + propertyInfo.Name;

				object propertyValue = ResolveAux(propertyInfo.PropertyType, fullPropertyName, resolvedObjects);

				Action<object, object> setter;

				if (!propertyActionDictionary.TryGetValue(fullPropertyName, out setter))
				{
					setter = ReflectionCompiler.CreateSetter(propertyInfo);
					propertyActionDictionary[fullPropertyName] = setter;
				}

				setter(instance, propertyValue);
			}
		}
		
		object BuildUp(Type type, string key)
		{
			if (type == null)
			{
				throw new ResolutionException("type should not be null.");
			}

			object instance = null;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				ResolverDictionary resolverDictionary;
				if (typeDictionary.TryGetValue(type, out resolverDictionary))
				{
					Resolver resolver;
					if (resolverDictionary.TryGetValue(key, out resolver))
					{
						instance = resolver.GetObject();
					}
				}
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			if (instance != null)
			{
				return instance;
			}

			return Instantiate(type);
		}

		class ConstructorInvokeInfo
		{
			internal readonly ParameterInfo[] ParameterInfos;

			Func<object[], object> constructorFunc;

			internal Func<object[], object> ConstructorFunc
			{
				get
				{
					if (constructorFunc == null)
					{
						constructorFunc = ReflectionCompiler.CreateFunc(Constructor);
					}

					return constructorFunc;
				}
			}

			internal readonly ConstructorInfo Constructor;

			internal ConstructorInvokeInfo(ConstructorInfo constructor)
			{
				Constructor = constructor;
				ParameterInfos = constructor.GetParameters();
			}
		}

		object Instantiate(ConstructorInvokeInfo info)
		{
			var constructorParameters = info.ParameterInfos;
			var length = constructorParameters.Length;
			var parametersList = new object[length];
			for (int i = 0; i < length; i++)
			{
				ParameterInfo parameterInfo = constructorParameters[i];
				object parameter = Resolve(parameterInfo.ParameterType);
				if (parameter == null && !parameterInfo.IsOptional)
				{
					throw new ResolutionException(
						"Failed to instantiate parameter " + parameterInfo.Name);
				}

				parametersList[i] = parameter;
			}

			var constructorFunc = info.ConstructorFunc;
			try
			{
				//return info.Constructor.Invoke(parametersList.ToArray());
				return constructorFunc(parametersList);
			}
			catch (Exception ex)
			{
				throw new ResolutionException("Failed to resolve " + info.Constructor.DeclaringType, ex);
			}
		}

		object Instantiate(Type type)
		{
			ConstructorInvokeInfo invokeInfo;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				constructorDictionaryLockSlim.EnterReadLock();
			}

			try
			{
				if (constructorDictionary.TryGetValue(type, out invokeInfo))
				{
					return Instantiate(invokeInfo);
				}
			}
			finally
			{
				if (useLock)
				{
					constructorDictionaryLockSlim.ExitReadLock();
				}
			}

#if NETFX_CORE
			var constructors = type.GetTypeInfo().DeclaredConstructors.Where(x => !x.IsStatic && x.IsPublic).ToArray();
#else
			var constructors = type.GetConstructors();
#endif

			ConstructorInfo constructor = null;

			if (constructors.Length > 0)
			{
				ConstructorInfo bestMatch = null;
				int biggestLength = -1;

				foreach (ConstructorInfo constructorInfo in constructors)
				{
					var dependencyAttributes = constructorInfo.GetCustomAttributes(typeof(InjectDependenciesAttribute), false);
#if NETFX_CORE
					var attributeCount = dependencyAttributes.Count();
#else
					var attributeCount = dependencyAttributes.Length;
#endif
					bool hasAttribute = attributeCount > 0;

					if (hasAttribute)
					{
						constructor = constructorInfo;
						break;
					}

					var length = constructorInfo.GetParameters().Length;

					if (length > biggestLength)
					{
						biggestLength = length;
						bestMatch = constructorInfo;
					}
				}

				if (constructor == null)
				{
					constructor = bestMatch;
				}
			}
			else
			{
#if !NETFX_CORE
				ConstructorInfo bestMatch = null;
				int biggestLength = -1;

				constructors = type.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);

				if (constructors.Length > 0)
				{
					foreach (ConstructorInfo constructorInfo in constructors)
					{
						if (constructorInfo.GetCustomAttributes(typeof(InjectDependenciesAttribute), false).Length > 0)
						{
							constructor = constructorInfo;
							break;
						}

						var length = constructorInfo.GetParameters().Length;

						if (length > biggestLength)
						{
							biggestLength = length;
							bestMatch = constructorInfo;
						}
					}

					if (constructor == null)
					{
						constructor = bestMatch;
					}
				}
#endif
			}

			if (constructor == null)
			{
				throw new ResolutionException(
					"Could not locate a constructor for " + type.FullName);
			}

			invokeInfo = new ConstructorInvokeInfo(constructor);

			if (useLock)
			{
				constructorDictionaryLockSlim.EnterWriteLock();
			}

			try
			{
				constructorDictionary[type] = invokeInfo;
			}
			finally
			{
				if (useLock)
				{
					constructorDictionaryLockSlim.ExitWriteLock();
				}
			}

			return Instantiate(invokeInfo);
		}

		static string GetKeyValueOrDefault(string key)
		{
			if (string.IsNullOrWhiteSpace(key))
			{
				key = defaultKey;
			}

			return key;
		}
	}
}
